﻿/*!
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Pittypat.Web
{
    /// <summary>
    /// 处理文件的上传。
    /// </summary>
    static class UploadSocketHandler
    {
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(UploadSocketHandler));

        #region FileInfo

        sealed class FileInfo
        {
            internal FileInfo(string name, long size)
            {
                this.Name = name ?? "";
                this.Size = size < 0L ? 0L : size;
                this.Type = Mime.Of(Path.GetExtension(name));
            }

            internal string Name { get; set; }
            internal long Size { get; set; }
            internal string Type { get; set; }
        }

        #endregion

        /// <summary>
        /// 根据配置文件中的功能的 uploadPath 定义的路径模板创建一个新的虚拟路径，并替换模板中的：{ext}、{uuid}、{year}、{month}、{day}、{hour}、{minute}、{second}、{ms}。
        /// </summary>
        /// <param name="template">uploadPath 定义的模板。</param>
        /// <param name="userId">当前用户的 Id，可能为匿名用户。</param>
        /// <param name="ext">文件的扩展名称。</param>
        /// <returns>文件的虚拟路径。</returns>
        static string CreateVirtualPath(string template, @uuid? userId, string ext)
        {
            if (string.IsNullOrEmpty(template))
            {
                return "";
            }

            var now = DateTime.Now;
            var s = template;

            if (userId.HasValue)
            {
                s = Regex.Replace(s, "{userid}", userId.Value.ToString("N"), RegexOptions.IgnoreCase);
            }

            s = Regex.Replace(s, "{ext}", ext ?? "", RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{uuid}", Guid.NewGuid().ToString("N"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{year}", now.Year.ToString("D4"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{month}", now.Month.ToString("D2"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{day}", now.Day.ToString("D2"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{hour}", now.Hour.ToString("D2"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{minute}", now.Minute.ToString("D2"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{second}", now.Second.ToString("D2"), RegexOptions.IgnoreCase);
            s = Regex.Replace(s, "{ms}", now.Millisecond.ToString("D3"), RegexOptions.IgnoreCase);
            
            return s;
        }

        // 从查询字符串中分析要上传的文件的信息，查询字符串格式为：
        // size={所有文件总字节数}&count={文件总数目}&n1={第一个文件名称}&z1={第一个文件大小}...
        // 一次能够上传的文件数目由服务器可以处理的查询字符串的最大长度限制。
        static string GetFileInfo(Request request, out List<FileInfo> files)
        {
            files = null;
            long totalSize = 0L;
            int count = 0;

            if (!long.TryParse(request.Form["size"], out totalSize) || totalSize <= 0)
            {
                return "没有提供有效的要上传文件的总字节数。\r\n没有选择任何文件，或者是一个编码错误，需要开发人员检查文件上传代码。";
            }

            if (!int.TryParse(request.Form["count"], out count) || count <= 0)
            {
                return "没有提供有效的要上传文件的总数目。\r\n没有选择任何文件，或者是一个编码错误，需要开发人员检查文件上传代码。";
            }

            files = new List<FileInfo>(count);
            string name = "";
            long size = 0L;

            for (int i = 0; i < count; ++i)
            {
                name = request.Form[string.Format("n{0}", i + 1)];
                if (string.IsNullOrEmpty(name))
                {
                    files = null;
                    return string.Format("没有提供第 {0} 个文件的文件名称。\r\n这可能是一个编码错误，需要开发人员检查文件上传代码。", i + 1);
                }

                if (!long.TryParse(request.Form[string.Format("z{0}", i + 1)], out size) || size <= 0)
                {
                    files = null;
                    return string.Format("没有提供 {0} 的文件大小。\r\n这可能是一个编码错误，需要开发人员检查文件上传代码。", name);
                }

                files.Add(new FileInfo(name, size));
            }

            return "";
        }

        #region UploadInfo

        sealed class UploadInfo
        {
            internal int Preload { get; set; }
        }

        #endregion

        static async Task<UploadFile> ReadAndSaveFileAsync(FileInfo info, Request request, string virtualTemplate, byte[] buffer, UploadInfo uploadInfo)
        {
            var progress = (WebSocketAsyncProgress)request.Progress;
            var error = false;
            var physicalPath = "";

            try
            {
                var extension = Path.GetExtension(info.Name);
                var read = 0L;
                var count = -1;
                var fileContentBytes = 0;
                var uploadingMessage = new UploadingMessage(0);
                var virtualPath = CreateVirtualPath(virtualTemplate, request.User.Id, extension);
                
                physicalPath = VirtualFile.MapPath(virtualPath);
                var name = Path.GetFileName(physicalPath);

                // 确保目标路径是存在的
                var dir = Path.GetDirectoryName(physicalPath);
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }

                using (var stream = new FileStream(physicalPath, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    // 如果存在已经预先加载的数据，首先写入文件
                    if (uploadInfo.Preload > 0)
                    {
                        await stream.WriteAsync(buffer, 0, uploadInfo.Preload, progress.Token);
                        read += uploadInfo.Preload;
                        uploadInfo.Preload = 0;
                    }

                    while (read < info.Size)
                    {
                        count = await progress.ReceiveAsync(buffer);
                        if (count <= 0)
                        {
                            // 发生错误，或者连接关闭
                            error = true;
                            break;
                        }
                        
                        // 写入文件
                        if (read + count <= info.Size)
                        {
                            // 刚刚读取的数据仍然是当前文件的数据
                            await stream.WriteAsync(buffer, 0, count, progress.Token);
                            read += count;
                        }
                        else
                        {
                            // 刚刚读取的数据中包含了其他文件的数据
                            fileContentBytes = (int)((read + count) - info.Size);
                            await stream.WriteAsync(buffer, 0, fileContentBytes, progress.Token);
                            read += fileContentBytes;

                            // 缓冲区中还包含了下一个文件的数据
                            uploadInfo.Preload = count - fileContentBytes;
                            Buffer.BlockCopy(buffer, fileContentBytes, buffer, 0, uploadInfo.Preload);
                        }
                        
                        if (read >= info.Size)
                        {
                            // 整个文件传送完毕
                            await progress.SendAsync(new UploadedMessage(virtualPath, info.Size));
                        }
                        else
                        {
                            // 发送 uploading 消息
                            uploadingMessage.Size = count;
                            await progress.SendAsync(uploadingMessage);
                        }
                    }
                }

                if (error)
                {
                    try
                    {
                        File.Delete(physicalPath);
                    }
                    catch
                    {
                    }

                    return null;
                }

                return new UploadFile(virtualPath, physicalPath, name, info.Name, extension, info.Size, info.Type);
            }
            catch (Exception exc)
            {
                Log.Fatal(exc.Message, exc);

                try
                {
                    File.Delete(physicalPath);
                }
                catch
                {
                }

                await progress.SendAsync(Message.Create(exc));
            }

            return null;
        }

        static bool IsExtensionAllowed(string[] exts, string ext)
        {
            foreach (var item in exts)
            {
                if (string.Compare(item, ext, true) == 0)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// 异步处理指定的请求。
        /// </summary>
        /// <param name="request">要处理的请求。</param>
        /// <returns>结果消息。</returns>
        internal static async Task ProcessAsync(Request request)
        {
            var progress = (WebSocketAsyncProgress)request.Progress;

            var virtualPath = request.UploadVirtualPath;

            if (string.IsNullOrEmpty(virtualPath) || request.MaxUploadSize <= 0L)
            {
                await progress.SendAsync(Message.Create(string.Format("没有为功能 {0} 指定上传文件的保存位置，或者没有定义最大尺寸，或者最大尺寸小于等于 0。\r\n这是一个配置错误，需要系统管理人员调整配置文件。", request.LowerAbsPath)));
                return;
            }

            List<FileInfo> fileInfos = null;

            var msg = GetFileInfo(request, out fileInfos);

            if (!string.IsNullOrEmpty(msg))
            {
                await progress.SendAsync(Message.Create(msg));
                return;
            }

            if (fileInfos == null || fileInfos.Count == 0)
            {
                await progress.SendAsync(Message.Create("没有要上传的文件。\r\n没有选择任何文件，或者是一个编码错误，需要开发人员检查文件上传代码。"));
                return;
            }

            // 检查上传文件的尺寸是否超过限制
            foreach (var item in fileInfos)
            {
                if (item.Size > request.MaxUploadSize)
                {
                    await progress.SendAsync(Message.Create(string.Format("文件 {0} 的长度 {1:F2} KB 超过了允许的最大长度 {2:F2} KB。\r\n请选择更小的文件，或者询问系统管理人员，通过修改配置文件的方式来增加文件的最大长度。",
                        item.Name, item.Size / 1024d, request.MaxUploadSize / 1024d)));
                    return;
                }
            }

            // 检查扩展名称是否满足要求
            if (request.AllowExtensions == null || request.AllowExtensions.Length == 0)
            {
                await progress.SendAsync(Message.Create(string.Format("没有为功能 {0} 指定上传文件的类型，将阻止上传任何类型的文件。\r\n这是一个配置错误，需要系统管理人员调整配置文件。", request.LowerAbsPath)));
                return;
            }

            var ext = "";
            foreach (var item in fileInfos)
            {
                ext = Path.GetExtension(item.Name);
                if (!IsExtensionAllowed(request.AllowExtensions, ext))
                {
                    await progress.SendAsync(Message.Create(string.Format("扩展名称为 {0} 的文件不允许。\r\n这是一个安全选项，请选择其他类型的文件，或者将文件的扩展名称修改为允许的扩展名称。", ext)));
                    return;
                }
            }

            var files = new List<UploadFile>(fileInfos.Count);
            UploadFile file = null;
            // 读取数据使用的缓冲区
            var buffer = request.ReceiveBuffer ?? new byte[WebSocketHelper.MaxBufferSize];
            // 已经提前加载到缓冲区中的数据
            var uploadInfo = new UploadInfo { Preload = 0 };

            // 如果是正在接收第一个文件，则首先发送第一个 uploaded 消息，表示服务器已经准备好接收数据，同时向客户端发送文件分片的尺寸 maxBufferSize。
            await progress.SendAsync(new UploadedMessage("", WebSocketHelper.MaxBufferSize));

            try
            {
                foreach (var info in fileInfos)
                {
                    file = await ReadAndSaveFileAsync(info, request, virtualPath, buffer, uploadInfo);
                    if (file == null)
                    {
                        // 保存失败
                        UploadFile.Delete(files);
                        break;
                    }
                    else
                    {
                        files.Add(file);
                    }
                }
            }
            catch (Exception exc)
            {
                // 上传失败，删除已经保存的文件
                UploadFile.Delete(files);

                Log.Fatal("保存上传的文件失败。", exc);

                await progress.SendAsync(Message.Create(exc));
            }
        }
    }
}
